/*
 * Copyright 2008, Denis Zjukow.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jemframework.domain;

import java.util.Collection;

public abstract class AbstractToManyRelation<SourceType extends Entity, PublicTargetType extends Entity, PublicCollectionType extends Collection<PublicTargetType>, InternalTargetType extends PublicTargetType, InternalCollectionType extends Collection<InternalTargetType>>
    extends AbstractRelation<SourceType, PublicTargetType> {

  private InternalCollectionType iInternalCollection;
  private PublicCollectionType iPublicCollection;

  public AbstractToManyRelation(SourceType aSourceObject) {
    super(aSourceObject);
  }

  @SuppressWarnings("unchecked")
  public void addPublicTargetObject(PublicTargetType aTarget) {
    if (aTarget == null) {
      throw new NullPointerException();
    }

    ensureCollectionsAreNotNull();

    if (!containsTargetObject(aTarget)) {
      // Add the target to the public collection.
      // NOTE: It is okay to cast to the public target type as we use know that
      // the real objects type is InternalTargetType.
      getInternalTargetObjects().add((InternalTargetType) aTarget);

      // Add the new target to the public collection.
      getModifieblePublicCollection().add(aTarget);

      if (isBidirectional()) {
        updateTarget(aTarget);
      }
    }
  }

  protected boolean containsTargetObject(PublicTargetType aTarget) {
    if (isInternalCollectionEmptyOrNull()) {
      // The relation is empty i.e doesn't contain any target objects
      // -> return false.
      return false;
    }

    return iInternalCollection.contains(aTarget);
  }

  protected abstract InternalCollectionType createInternalCollection();

  protected abstract PublicCollectionType createPublicCollection();

  protected abstract PublicCollectionType createUnmodifieblePublicCollection(PublicCollectionType aCollection);

  protected void ensureCollectionsAreNotNull() {
    if (iInternalCollection == null) {
      iInternalCollection = createInternalCollection();
    }

    if (iPublicCollection == null) {
      iPublicCollection = createPublicCollection();
    }
  }

  protected abstract InternalCollectionType getEmptyInternalCollection();

  protected abstract PublicCollectionType getEmptyPublicCollection();

  public InternalCollectionType getInternalTargetObjects() {
    if (iInternalCollection == null) {
      return getEmptyInternalCollection();
    }

    return iInternalCollection;
  }

  protected PublicCollectionType getModifieblePublicCollection() {
    return iPublicCollection;
  }

  public PublicCollectionType getPublicTargetObjects() {
    if (isPublicCollectionEmptyOrNull()) {
      return getEmptyPublicCollection();
    }

    return iPublicCollection;
  }

  protected boolean isInternalCollectionEmptyOrNull() {
    return iInternalCollection == null || iInternalCollection.isEmpty();
  }

  protected boolean isPublicCollectionEmptyOrNull() {
    return iPublicCollection == null || iPublicCollection.isEmpty();
  }

  public void removePublicTargetObject(PublicTargetType aTarget) {
    if (aTarget == null) {
      throw new NullPointerException();
    }

    if (containsTargetObject(aTarget)) {
      // Remove the target from the internal collection.
      getInternalTargetObjects().remove(aTarget);

      // Remove the target from the public target.
      getModifieblePublicCollection().remove(aTarget);

      if (isBidirectional()) {
        resetTarget(aTarget);
      }
    }
  }

  public void setInternalTargetObjects(InternalCollectionType aTargetObjects) {
    if (aTargetObjects == null) {
      throw new NullPointerException();
    }

    // Assign the new internal target collection.
    iInternalCollection = aTargetObjects;

    ensureCollectionsAreNotNull();

    // Clear the public collection and, then, add the new targets.
    iPublicCollection.clear();
    iPublicCollection.addAll(aTargetObjects);
  }

  public void setPublicTargetObjects(PublicCollectionType aTargetObjects) {
    if (aTargetObjects == null) {
      throw new NullPointerException();
    }

    // Assign the new internal target collection.
    iPublicCollection = aTargetObjects;

    ensureCollectionsAreNotNull();

    // Clear the internal collection and, then, add the new targets.
    iInternalCollection.clear();
    for (PublicTargetType eachTarget : aTargetObjects) {
      iInternalCollection.add((InternalTargetType) eachTarget);
    }
  }

  @Override
  public void update() {
    if (isBidirectional()) {
      for (PublicTargetType each : getInternalTargetObjects()) {
        updateTarget(each);
      }
    }
  }
}
